﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedParameter.Local

/* Buffer.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

#endregion

#nullable enable

namespace AM.Skia.RichTextKit.Utils;

/// <summary>
/// A growable array of elements of type `T`
/// </summary>
/// <typeparam name="T">The buffer element type</typeparam>
[DebuggerDisplay ("Length = {Length}")]
public class Buffer<T>
    : IEnumerable<T>
{
    /// <summary>
    /// Constructs a new buffer.
    /// </summary>
    public Buffer()
    {
        // Create initial array
        _data = new T[32];
    }

    /// <summary>
    /// The data held by this buffer
    /// </summary>
    private T[] _data;

    /// <summary>
    /// The data held by this buffer
    /// </summary>
    public T[] Underlying => _data;

    /// <summary>
    /// The used length of the buffer
    /// </summary>
    [DebuggerBrowsable (DebuggerBrowsableState.Never)]
    private int _length;

    /// <summary>
    /// Gets or sets the length of the buffer
    /// </summary>
    /// <remarks>
    /// The internal buffer will be grown if the new length is larger
    /// than the current buffer size.
    /// </remarks>
    public int Length
    {
        get => _length;
        set
        {
            // Now grow buffer
            if (!GrowBuffer (value))
            {
                // If the length is increasing, but we didn't re-size the buffer
                // then we need to clear the new elements.
                if (value > _length)
                {
                    Array.Clear (_data, _length, value - _length);
                }
            }

            // Store new length
            _length = value;
        }
    }

    /// <summary>
    /// Ensures the buffer has sufficient capacity
    /// </summary>
    /// <param name="requiredLength"></param>
    /// <returns></returns>
    [MethodImpl (MethodImplOptions.AggressiveInlining)]
    private bool GrowBuffer (int requiredLength)
    {
        if (requiredLength <= _data.Length)
        {
            return false;
        }

        // Work out new length
        int newLength;
        if (_data.Length < 1048576)
        {
            // Below 1MB, grow by doubling...
            newLength = _data.Length * 2;
        }
        else
        {
            // otherwise grow by 1mb at a time...
            newLength = _data.Length + 1048576;
        }

        // Make sure we're allocating enough
        if (newLength < requiredLength)
        {
            newLength = requiredLength;
        }

        // Allocate new buffer, only copying _length, not Data.Length
        var newData = new T[requiredLength];
        Array.Copy (_data, 0, newData, 0, _length);
        _data = newData;

        return true;
    }

    /// <summary>
    /// Clears the buffer, keeping the internally allocated array.
    /// </summary>
    public void Clear()
    {
        _length = 0;
    }

    /// <summary>
    /// Inserts room into the buffer
    /// </summary>
    /// <param name="position">The position to insert at</param>
    /// <param name="length">The length to insert</param>
    /// <param name="clear">Whether to clear the inserted part of the buffer</param>
    /// <returns>The new buffer area as a slice</returns>
    public Slice<T> Insert (int position, int length, bool clear = true)
    {
        // Grow internal buffer?
        GrowBuffer (_length + length);

        // Shuffle existing to make room for inserted data
        if (position < _length)
        {
            Array.Copy (_data, position, _data, position + length, _length - position);
        }

        // Update the length
        _length += length;

        // Clear it?
        if (clear)
        {
            Array.Clear (_data, position, length);
        }

        // Return slice
        return SubSlice (position, length);
    }

    /// <summary>
    /// Insert a slice of data into this buffer
    /// </summary>
    /// <param name="position">The position to insert at</param>
    /// <param name="data">The data to insert</param>
    /// <returns>The newly inserted data as a slice</returns>
    public Slice<T> Insert (int position, Slice<T> data)
    {
        // Make room
        var slice = Insert (position, data.Length, false);

        // Copy in the source slice
        Array.Copy (data.Underlying, data.Start, _data, position, data.Length);

        // Return slice
        return slice;
    }

    /// <summary>
    /// Adds to the buffer, returning a slice of requested size
    /// </summary>
    /// <param name="length">Number of elements to add</param>
    /// <param name="clear">True to clear the content; otherwise false</param>
    /// <returns>A slice representing the allocated elements.</returns>
    public Slice<T> Add (int length, bool clear = true)
    {
        return Insert (_length, length, clear);
    }

    /// <summary>
    /// Add a slice of data to this buffer.
    /// </summary>
    /// <param name="slice">The slice to be added</param>
    /// <returns>A slice representing the added elements.</returns>
    public Slice<T> Add (Slice<T> slice)
    {
        var pos = _length;

        // Grow internal buffer?
        GrowBuffer (_length + slice.Length);
        _length += slice.Length;

        // Copy in the slice
        Array.Copy (slice.Underlying, slice.Start, _data, pos, slice.Length);

        // Return position
        return SubSlice (pos, slice.Length);
    }

    /// <summary>
    /// Delete a section of the buffer
    /// </summary>
    /// <param name="from">The position to delete from</param>
    /// <param name="length">The length to of the deletion</param>
    public void Delete (int from, int length)
    {
        // Clamp to buffer size
        if (from >= _length)
        {
            return;
        }

        if (from + length >= _length)
        {
            length = _length - from;
        }

        // Shuffle trailing data
        if (from + length < _length)
        {
            Array.Copy (_data, from + length, _data, from, _length - (from + length));
        }

        // Update length
        _length -= length;
    }

    /// <summary>
    /// Gets a reference to an element in the buffer
    /// </summary>
    /// <param name="index">The element index</param>
    /// <returns>A reference to the element value.</returns>
    public ref T this [int index]
    {
        [MethodImpl (MethodImplOptions.AggressiveInlining)]
        get
        {
            if (index < 0 || index >= _length)
            {
                throw new ArgumentOutOfRangeException (nameof (index));
            }

            return ref _data[index];
        }
    }

    /// <summary>
    /// Returns a range within this buffer as a <see cref="Slice{T}"/>
    /// </summary>
    /// <param name="start">Start offset of the slice</param>
    /// <param name="length">Length of the slice</param>
    /// <returns>A slice for the specified sub-range</returns>
    public Slice<T> SubSlice (int start, int length)
    {
        if (start < 0 || start + length > _length)
        {
            throw new ArgumentOutOfRangeException (
                $"Invalid buffer slice range ({start},{length}) with buffer length of {_length}");
        }

        return new Slice<T> (_data, start, length);
    }

    /// <summary>
    /// Returns the entire buffer contents as a <see cref="Slice{T}"/>
    /// </summary>
    /// <returns>A Slice</returns>
    public Slice<T> AsSlice()
    {
        return SubSlice (0, _length);
    }

    /// <summary>
    /// Split the utf32 buffer on a codepoint type
    /// </summary>
    /// <param name="delim">The delimiter</param>
    /// <returns>An enumeration of slices</returns>
    public IEnumerable<Slice<T>> Split (T delim)
    {
        var start = 0;
        for (var i = 0; i < Length; i++)
        {
            if (_data[i]!.Equals (delim))
            {
                yield return SubSlice (start, i - start);
                start = i + 1;
            }
        }

        yield return SubSlice (start, Length - start);
    }

    /// <summary>
    /// Split the utf32 buffer on a codepoint type
    /// </summary>
    /// <param name="delim">The delimiter to split on</param>
    /// <returns>An enumeration of offset/length for each range</returns>
    public IEnumerable<(int Offset, int Length)> GetRanges (T delim)
    {
        var start = 0;
        for (var i = 0; i < Length; i++)
        {
            if (_data[i]!.Equals (delim))
            {
                yield return (start, i - start);
                start = i + 1;
            }
        }

        yield return (start, Length - start);
    }

    /// <summary>
    /// Replaces all instances of a value in the buffer with another value
    /// </summary>
    /// <param name="oldValue">The value to replace</param>
    /// <param name="newValue">The new value</param>
    /// <returns>The number of replacements made</returns>
    public int Replace (T oldValue, T newValue)
    {
        var count = 0;
        for (var i = 0; i < Length; i++)
        {
            if (_data[i]!.Equals (oldValue))
            {
                _data[i] = newValue;
                count++;
            }
        }

        return count;
    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        return new ArraySliceEnumerator<T> (_data, 0, _length);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new ArraySliceEnumerator<T> (_data, 0, _length);
    }
}
